14. 线程安全

线程锁

线程加锁必须要注意的两点:

  1. 同步的锁对象可以是任意类型的对象。
  2. 这些加锁的线程必须使用同一个锁对象。

1. 同步代码块

1
2
3
4
5
6
语法格式:
synchronized(同步的锁对象){
需要锁起来的代码:一个线程在运行这段代码期间,不想别的线程半路插入
}

和共享数据相关的语句都要锁起来
Thread 的方式创建多线程并加锁

TicketService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.itguigu.ticket;

import java.util.ArrayList;

public class TicketService {
ArrayList<String> allTicket;

/**
* 构造方法创建所有的票
*/
public TicketService() {
allTicket = new ArrayList<>();
allTicket.add("01车01A");
allTicket.add("01车01B");
allTicket.add("01车01C");
allTicket.add("01车01D");
allTicket.add("01车01E");
allTicket.add("01车01F");

allTicket.add("02车01A");
allTicket.add("02车01B");
allTicket.add("02车01C");
allTicket.add("02车01D");
allTicket.add("02车01E");
allTicket.add("02车01F");
}

/**
* 是否还有票
* @return
*/
public boolean hasTicket() {
return allTicket.size() > 0;
}

/**
* 卖票
* @return
*/
public String sellTicket() {
return allTicket.remove(0);
}
}

TestThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.itguigu.ticket;

public class TestThread {
public static void main(String[] args) {
BuyTicketByThread buyTicketByThread1 = new BuyTicketByThread("窗口1");
BuyTicketByThread buyTicketByThread2 = new BuyTicketByThread("窗口2");

buyTicketByThread1.start();
buyTicketByThread2.start();
}
}

class BuyTicketByThread extends Thread{
// 因为创建两个线程就要 new 两次 BuyTicketByThread 方法,所以如果要让两个线程之间保持同一份 ticketService
// 那么就应该使 ticketService 为 static。这样才好对不同对象中同一个资源变量进行加锁
private static TicketService ticketService = new TicketService();

public BuyTicketByThread(String name) {
super(name);
}

@Override
public void run() {
while (true) {
// 使用 synchronized 关键字进行加锁,ticketService 是加锁的对象
// synchronized 后面是锁住的操作
synchronized (ticketService) {
if (ticketService.hasTicket()) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " 卖了:" + ticketService.sellTicket());
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
Runnable 的方式创建多线程并加锁

TicketService.java(和上面一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.itguigu.ticket;

import java.util.ArrayList;

public class TicketService {
ArrayList<String> allTicket;

/**
* 构造方法创建所有的票
*/
public TicketService() {
allTicket = new ArrayList<>();
allTicket.add("01车01A");
allTicket.add("01车01B");
allTicket.add("01车01C");
allTicket.add("01车01D");
allTicket.add("01车01E");
allTicket.add("01车01F");

allTicket.add("02车01A");
allTicket.add("02车01B");
allTicket.add("02车01C");
allTicket.add("02车01D");
allTicket.add("02车01E");
allTicket.add("02车01F");
}

/**
* 是否还有票
* @return
*/
public boolean hasTicket() {
return allTicket.size() > 0;
}

/**
* 卖票
* @return
*/
public String sellTicket() {
return allTicket.remove(0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.itguigu.ticket;

public class TestRunnable {
public static void main(String[] args) {
BuyTicketByRunnable buyTicketByRunnable = new BuyTicketByRunnable();
Thread thread1 = new Thread(buyTicketByRunnable, "窗口1");
Thread thread2 = new Thread(buyTicketByRunnable, "窗口2");

thread1.start();
thread2.start();
}
}

class BuyTicketByRunnable implements Runnable{
// 因为这里使用的是 Runnable 的方式创建的线程,且只 new 了一个 BuyTicketByRunnable 对象
// 所以在两个线程中 ticketService 只有一份,我们就不用像在 Thread 中一样为此变量加上 static 了。
private TicketService ticketService = new TicketService();

@Override
public void run() {
while (true) {
// 使用 synchronized 关键字进行加锁,ticketService 是加锁的对象
// synchronized 后面是锁住的操作
// 因为只 new 了一个 BuyTicketByRunnable 对象,所以两个线程中其实只有一个 BuyTicketByRunnable 对象
// 所以这里可以将 ticketService 换为 this(this即BuyTicketByRunnable 对象),这样也能达到效果
synchronized (ticketService) {
if (ticketService.hasTicket()) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " 卖了:" + ticketService.sellTicket());
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
总结

在使用 synchronized 进行加锁的时候,在 Thread 创建的线程中,需要有一个静态的多个线程多只有一份变量,即上面的 private static TicketService ticketService = new TicketService(); 而 Runnable 则不需要此变量为静态,即使用 private TicketService ticketService = new TicketService(); 即可,而且 Runnable 还可以将锁对象选定为当前的 this,Thread 则不可以。在其他使用方面,因为是实现的 Runnable 接口,所以线程类还可以继承其他的类,而如果使用 Thread 的话,那就不能继续继承其他的类的,因为 Java 里面只能单继承。所以综上考虑应该优先使用 Runnable。

2. 同步方法

如果一次任务是在一个方法中完成的,那么可以对这个方法直接上锁,即在方法上加上 synchronized 关键字。

1
2
语法格式:
【修饰符】synchronized 返回值类型 方法名(【形参列表】)【throws 异常列表】{}

同步方法的锁对象

非静态方法:同步方法的锁对象就是当前的 this 对象

静态方法:当前类的 Class 对象(每一个类型被加载到内存后都会生成一个 Class 对象来表示这个类型,只要是同一种类型,那么 Class 对象就是同一个)

Thread 的方式创建多线程并加锁

在之前同步代码块的基础上将同步代码抽取为一个同步方法,而这里使用的是继承 Thread 的方式创建的线程,所以有多个 BuyTicketByThread,这时我们的同步方法就应该是静态的。因为这样才保证了多个线程使用的是同一个锁对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.itguigu.ticket;

public class TestThread {
public static void main(String[] args) {
BuyTicketByThread buyTicketByThread1 = new BuyTicketByThread("窗口1");
BuyTicketByThread buyTicketByThread2 = new BuyTicketByThread("窗口2");

buyTicketByThread1.start();
buyTicketByThread2.start();
}
}

class BuyTicketByThread extends Thread{
private static TicketService ticketService = new TicketService();

public BuyTicketByThread(String name) {
super(name);
}

@Override
public void run() {
// 这里不再是 true,这里使用 ticketService.hasTicket() 判断程序是否需要结束
while (ticketService.hasTicket()) {
saleOneTicket();
}
}

/**
* 因为线程是继承 Thread 类实现的,所以不能使用 this 对象作为锁对象。要选用多个线程之间都是共有的
* 所以我们将同步方法 saleOneTicket 修改为静态方法。这样就能达到目的。因为是静态方法,所以这里
* 的同步方法的锁对象就是当前类的 Class 对象。
*/
public static synchronized void saleOneTicket() {
// 这里使用 ticketService.hasTicket() 来控制线程之间的安全
if (ticketService.hasTicket()) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " 卖了:" + ticketService.sellTicket());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Runnable 的方式创建多线程并加锁

在之前同步代码块的基础上将同步代码抽取为一个同步方法,所以一个 BuyTicketByRunnable 对象也能启动多个线程。所以这种方式的实现是可以使用 this 对象【BuyTicketByRunnable 对象】作为锁对象的。那么这里就不需要加 static 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.itguigu.ticket;

public class TestRunnable {
public static void main(String[] args) {
BuyTicketByRunnable buyTicketByRunnable = new BuyTicketByRunnable();
Thread thread1 = new Thread(buyTicketByRunnable, "窗口1");
Thread thread2 = new Thread(buyTicketByRunnable, "窗口2");

thread1.start();
thread2.start();
}
}

class BuyTicketByRunnable implements Runnable{
private TicketService ticketService = new TicketService();

@Override
public void run() {
while (ticketService.hasTicket()) {
saleOneTicket();
}
}

/**
* 线程的实现因为是依靠实现 Runnable 接口实现的。所以一个 BuyTicketByRunnable 对象也能启动多个线程。
* 所以这种方式的实现是可以使用 this 对象【BuyTicketByRunnable 对象】作为锁对象的。那么这里就不需要加 static 了。因为是非静态方法,所以这里的同步方法的锁对象就是当前的 this 对象。
*/
public synchronized void saleOneTicket() {
if (ticketService.hasTicket()) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " 卖了:" + ticketService.sellTicket());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}